package nl.science.uva.pico.compiler;

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;

import jjtraveler.Visitable;
import nl.science.uva.pico.compiler.stacklanguage.StackInstructionFactory;
import nl.uva.science.pico.picolanguage.Factory;
import nl.uva.science.pico.picolanguage.types.EXP;
import nl.uva.science.pico.picolanguage.types.IDTYPEList;
import nl.uva.science.pico.picolanguage.types.PICOID;
import nl.uva.science.pico.picolanguage.types.PROGRAM;
import nl.uva.science.pico.picolanguage.types.STATEMENT;
import nl.uva.science.pico.picolanguage.types.STATEMENTList;
import nl.uva.science.pico.picolanguage.types.STATEMENTS;
import nl.uva.science.pico.picolanguage.types.idtype.Variable;
import nl.uva.science.pico.picolanguage.types.statement.Assignment;
import nl.uva.science.pico.picolanguage.types.statement.Conditional;
import nl.uva.science.pico.picolanguage.types.statement.Loop;
import nl.uva.science.pico.stacklanguage.types.LabelInstr;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import aterm.pure.PureFactory;

/**
 * The compiler converts the incoming pico into stacklanguage statements
 * 
 * @author Ivo van Dongen, Karel Pieterson
 *
 * @version 1.0 
 */
public class Compiler {
	private Logger logger = LoggerFactory.getLogger(Compiler.class);
	
	private static final int BLOCK_SIZE = 4;

	private StackInstructionFactory stackInstructionFactory;
	private List<LabelInstr> stackInstructionsList;
	private Factory picoFactory;
	
	public String compileStream(InputStream stream) throws IOException {
		//Initialize state
		setupFactories();
		stackInstructionsList = new ArrayList<LabelInstr>();
		 
		//Parse the input
		PROGRAM program = picoFactory.PROGRAMFromFile(stream);
		
		//Process declarations
		IDTYPEList declarations = (IDTYPEList) program.getChildAt(0).getChildAt(0).getChildAt(0);
		processDeclarations(declarations);
		
		//Process statements
		STATEMENTS statements = program.getBody();
		stackInstructionsList.addAll(processStatements(statements));
		
		//Add Stop()
		stackInstructionsList.add(stackInstructionFactory.createStopInstruction());
		
		//Return the stack program
		return stackInstructionFactory.createInstrList(stackInstructionsList).toString();
	}
	
	private void setupFactories() {
		//Set up our factories
		PureFactory pureFactory = new PureFactory();
		picoFactory = Factory.getInstance(pureFactory);
		stackInstructionFactory = new StackInstructionFactory(nl.uva.science.pico.stacklanguage.Factory.getInstance(pureFactory));
	}

	/**
	 * Processes variable declarations
	 * 
	 * @param declarations
	 */
	private void processDeclarations(IDTYPEList declarations) {

		logger.debug("Processing declarations: " + declarations);
		
		for (int indexChild = 0; indexChild < declarations.getChildCount(); indexChild++) {
			Variable variable = (Variable)declarations.getChildAt(indexChild);
			stackInstructionsList.add(stackInstructionFactory.createDeclaration(variable));
		}
	}
	
	/**
	 * Processes {@link STATEMENTS}
	 * 
	 * @param statements
	 * @return instructionList with generated instructions
	 * 
     * @throws UnsupportedOperationException if argument passed is not recognized as a valid statement
	 */
	private List<LabelInstr> processStatements(STATEMENTS statements) {

		logger.debug("Processing statements: " + statements);
		
		List<LabelInstr> instructionList = new ArrayList<LabelInstr>();

		//Check input
		if (statements == null || !statements.hasList() || statements.getList().getChildCount() == 0) {
			return instructionList;
		}
		//statements found in param
		STATEMENTList statementsList = statements.getList();
		
		//Loop over all the statements
		for (int indexChild = 0; indexChild < statementsList.getChildCount(); indexChild++) {
			
			//If this is a statement process it
			if(statementsList.getChildAt(indexChild) instanceof STATEMENT) {
				STATEMENT statement = (STATEMENT) statementsList.getChildAt(indexChild);
				
				if (statement instanceof Loop) {
					instructionList.addAll(processLoop((Loop) statement));
				} else if (statement instanceof Assignment) {
					instructionList.addAll(processAssignment((Assignment) statement));
				} else if (statement instanceof Conditional) {
					instructionList.addAll(processConditional((Conditional) statement));
				} else {
					//New type of statement??
					throw new UnsupportedOperationException("Unsupported type: " + statement + " class(" + statement.getClass() + ")");
				}
			} else {
				//No statement?? 
				throw new UnsupportedOperationException("Unrecognized type: "  + statementsList.getChildAt(indexChild) + 
														" class(" + statementsList.getChildAt(indexChild).getClass() + ")");
			}
		}
		return instructionList;
	}
	
	/**
	 * Processes a {@link Conditional}
	 * <ul>
	 * <li>Process the statements in the correct order</li>
	 * <li>Assemble the stack instructions in the correct execution order</li>
	 * </ul>
	 * 
	 * The first phase (Process) processes the statements in the correct order (this way the generated labels are correct):
	 * <ul>
	 * <li>Temporary increase the stack size</li>
	 * <li>Condition resolution</li>
	 * <li>Condition evaluation</li>
	 * <li>Correct the stack size</li>
	 * <li>Generate else body statements</li>
	 * <li>Generate if body statements</li>
	 * </ul>
	 * 
	 * The second phase assembles the pieces in the correct order
	 * <ul>
	 * <li>Jump label</li>
	 * <li>Condition expression resolution</li>
	 * <li>Condition evaluation</li>
	 * <li>Else branch statements</li>
	 * <li>Goto skip #2</li>
	 * <li>Skip #1</li>
	 * <li>If branch statements</li>
	 * <li>Skip #2</li>
	 * </ul>
	 * 
	 * @param loop the {@link Loop} to process
	 * @return the assembled {@link List} of stack machine statements	 
	 * 
     * @throws NullPointerException if argument passed is null
     * @throws IllegalStateException if argument doesn't contain a (valid) condition
     * 
	 */
	private List<LabelInstr> processConditional(Conditional conditional) {
		logger.debug("Processing conditional: " + conditional);
		
		//Check input 
		if(conditional == null) {
			throw new NullPointerException("Conditional is null!");
		}
		if(conditional.getCondition() == null) {
			throw new IllegalStateException("Conditional must have a condition");
		}
		if(conditional.getIfBranch() == null) {
			throw new IllegalStateException("Conditional must have an if branch");
		}
		
		/** Phase 1: process the statements in the correct order */
		//Condition resolution
		List<LabelInstr> conditionResolution = stackInstructionFactory.createExpInstructionList(conditional.getCondition());
		//Condition evaluation
		List<LabelInstr> conditionEvaluation = stackInstructionFactory.createIfEqualToCurrentStack(0);
		
		//Else body statements
		List<LabelInstr> elseBranchStatements;
		if(conditional.getElseBranch() != null && conditional.getElseBranch().getChildCount() > 0) {
			elseBranchStatements = processStatements(conditional.getElseBranch());
		} else {
			elseBranchStatements = new ArrayList<LabelInstr>();
		}
		
		//if body statements
		List<LabelInstr> ifBranchStatements = processStatements(conditional.getIfBranch());
		//Skip #1
		LabelInstr skip1 = stackInstructionFactory.createSkipInstruction();
		//Jump value (Label skip #1) == current
		LabelInstr jumpValue = stackInstructionFactory.createLabelReferenceInstruction(0);
		//Skip #2
		LabelInstr skip2 = stackInstructionFactory.createSkipInstruction();
		//GOTO skip #2
		List<LabelInstr> gotoSkip2 = stackInstructionFactory.createGotoInstruction(skip2);
		
		/** Phase 2: assemble the stack instructions in the correct execution order */
		List<LabelInstr> instructions = new ArrayList<LabelInstr>();
		
		//Jump value (Label skip #1)
		instructions.add(jumpValue);
		//Condition resolution
		instructions.addAll(conditionResolution);
		//Condition evaluation (true => GOTO skip #1)
		instructions.addAll(conditionEvaluation);
		//Else branch statements
		instructions.addAll(elseBranchStatements);
		//GOTO skip #2
		instructions.addAll(gotoSkip2);
		//Skip #1
		instructions.add(skip1);
		//If branch statements
		instructions.addAll(ifBranchStatements);
		//Skip #2
		instructions.add(skip2);
		
		return instructions;
	}
	
	/**
	 * Processes a loop. This has two phases:
	 * <ul>
	 * <li>Process</li>
	 * <li>Assemble</li>
	 * </ul>
	 * 
	 * The first phase (Process) processes the statements in the correct order (this way the generated labels are correct):
	 * <ul>
	 * <li>Condition expression resolution</li>
	 * <li>Condition evaluation</li>
	 * <li>Loop body</li>
	 * <li>Start jump label</li>
	 * <li>Goto start jump label</li>
	 * <li>Skip statement</li>
	 * </ul>
	 * 
	 * The second phase assembles the pieces in the correct order
	 * <ul>
	 * <li>Start jump label</li>
	 * <li>Condition expression resolution</li>
	 * <li>Condition evaluation</li>
	 * <li>Loop body</li>
	 * <li>Goto start jump label</li>
	 * <li>Skip statement</li>
	 * </ul>
	 * 
	 * @param loop the {@link Loop} to process
	 * @return the assembled {@link List} of stack machine statements
	 * 
     * @throws NullPointerException if argument passed is null
     * @throws IllegalStateException if argument doesn't contain a (valid) condition or body
	 * 
	 */
	private List<LabelInstr> processLoop(Loop loop) {

		logger.debug("Processing loop: " + loop);
		
		//Check input
		if (loop == null) {
			throw new NullPointerException("Loop is null!");
		}
		if (loop.getCondition() == null) {
			throw new IllegalArgumentException("Loop needs a condition: " + loop);
		}
		if (loop.getBody() == null) {
			throw new IllegalArgumentException("Loop needs a body: " + loop);
		}
		
		/** Phase 1: process the statements in the correct order */
		//Condition Expression
		List<LabelInstr> conditionExpressionInstructions = stackInstructionFactory.createExpInstructionList(loop.getCondition());
		//Condition evaluation
		List<LabelInstr> conditionEvaluationInstructions = stackInstructionFactory.createIfEqualToCurrentStack(0);
		
		//Body
		List<LabelInstr> bodyInstructions = processStatements(loop.getBody());
		//Jump label
		LabelInstr jumpLabelInstruction = stackInstructionFactory.createLabelReferenceInstruction(BLOCK_SIZE);
		//GOTO (start label)
		List<LabelInstr> gotoInstructions = stackInstructionFactory.createGotoInstruction(jumpLabelInstruction);
		//SKIP
		LabelInstr skipInstruction = stackInstructionFactory.createSkipInstruction();
		
		/** Phase 2: assemble the stack instructions in the correct execution order */
		List<LabelInstr> instructions = new ArrayList<LabelInstr>();
		//Start jump label
		instructions.add(jumpLabelInstruction);
		//Condition expression resolution
		instructions.addAll(conditionExpressionInstructions);
		//Condition evaluation
		instructions.addAll(conditionEvaluationInstructions);
		//Loop body
		instructions.addAll(bodyInstructions);
		//Goto start jump label
		instructions.addAll(gotoInstructions);
		//Skip statement
		instructions.add(skipInstruction);
		
		return instructions;
	}
	
	/**
	 * 
	 * @param assignment
	 * @return
	 */
	private List<LabelInstr> processAssignment(Assignment assignment) {
		logger.debug("Processing assignment: " + assignment);
		
		//Do some sanity checks
		if (assignment == null) {
			throw new NullPointerException("Null assignment!");
		}
		if (assignment.getChildCount() < 2) {
			throw new IllegalArgumentException("Assignment has to few children (at least 2 needed): " + assignment);
		}
		if (! (assignment.getChildAt(0) instanceof PICOID)) {
			throw new IllegalArgumentException("Cannot assign a to a non-variable: " + assignment.getChildAt(0) + " in assignment: " + assignment);
		}
		
		List<LabelInstr> gatheredInstructions = new ArrayList<LabelInstr>();
		
		//Start at the second child ( the fist is the variable reference to store it in)
		for (int index = 1; index < assignment.getChildCount(); index++) {
			Visitable child = assignment.getChildAt(index);
			
			if (!(child instanceof EXP)) {
				throw new IllegalStateException("Assignment cannot have non-expression children: " + child + " in assignment: " + assignment);
			} else {
				gatheredInstructions.addAll(stackInstructionFactory.createExpInstructionList((EXP) child));
			}
			
		}
		
		//Now add the store instruction
		gatheredInstructions.addAll(stackInstructionFactory.createStoreInstruction((PICOID) assignment.getChildAt(0)));
		
		return gatheredInstructions;
	}

}
